'use strict';

import {
  discardEmulationPrevention,
  picParameterSet,
  seqParameterSet
} from '../bit-streams/h264';

import {nalParseAVCC, mergePS} from './common/nal-parse';
import dataToHex from './common/data-to-hex.js';

  /**
   * Returns the string representation of an ASCII encoded four byte buffer.
   * @param buffer {Uint8Array} a four-byte buffer to translate
   * @return {string} the corresponding string
   */
const parseType = function (buffer) {
  var result = '';
  result += String.fromCharCode(buffer[0]);
  result += String.fromCharCode(buffer[1]);
  result += String.fromCharCode(buffer[2]);
  result += String.fromCharCode(buffer[3]);
  return result;
};

const parseMp4Date = function (seconds) {
  return new Date(seconds * 1000 - 2082844800000);
};

const parseSampleFlags = function (flags) {
  return {
    isLeading: (flags[0] & 0x0c) >>> 2,
    dependsOn: flags[0] & 0x03,
    isDependedOn: (flags[1] & 0xc0) >>> 6,
    hasRedundancy: (flags[1] & 0x30) >>> 4,
    paddingValue: (flags[1] & 0x0e) >>> 1,
    isNonSyncSample: flags[1] & 0x01,
    degradationPriority: (flags[2] << 8) | flags[3]
  };
};

let lastSPS;
let lastPPS;
let lastOptions;

// registry of handlers for individual mp4 box types
const parse = {
  // codingname, not a first-class box type. stsd entries share the
  // same format as real boxes so the parsing infrastructure can be
  // shared
  avc1: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    return {
      dataReferenceIndex: view.getUint16(6),
      width: view.getUint16(24),
      height: view.getUint16(26),
      horizresolution: view.getUint16(28) + (view.getUint16(30) / 16),
      vertresolution: view.getUint16(32) + (view.getUint16(34) / 16),
      frameCount: view.getUint16(40),
      depth: view.getUint16(74),
      config: inspectMp4(data.subarray(78, data.byteLength))
    };
  },
  avcC: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        configurationVersion: data[0],
        avcProfileIndication: data[1],
        profileCompatibility: data[2],
        avcLevelIndication: data[3],
        lengthSizeMinusOne: data[4] & 0x03,
        sps: [],
        pps: []
      },
      numOfSequenceParameterSets = data[5] & 0x1f,
      numOfPictureParameterSets,
      nalSize,
      offset,
      i;

    // iterate past any SPSs
    offset = 6;
    for (i = 0; i < numOfSequenceParameterSets; i++) {
      nalSize = view.getUint16(offset);
      offset += 2;
      var nalData = discardEmulationPrevention(new Uint8Array(data.subarray(offset + 1, offset + nalSize)));
      lastSPS = seqParameterSet.decode(nalData);
      lastOptions = mergePS(lastPPS, lastSPS);
      result.sps.push(lastSPS);
      offset += nalSize;
    }
    // iterate past any PPSs
    numOfPictureParameterSets = data[offset];
    offset++;
    for (i = 0; i < numOfPictureParameterSets; i++) {
      nalSize = view.getUint16(offset);
      offset += 2;
      var nalData = discardEmulationPrevention(new Uint8Array(data.subarray(offset + 1, offset + nalSize)));
      lastPPS = picParameterSet.decode(nalData);
      lastOptions = mergePS(lastPPS, lastSPS);
      result.pps.push(lastPPS);
      offset += nalSize;
    }
    return result;
  },
  btrt: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    return {
      bufferSizeDB: view.getUint32(0),
      maxBitrate: view.getUint32(4),
      avgBitrate: view.getUint32(8)
    };
  },
  esds: function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      esId: (data[6] << 8) | data[7],
      streamPriority: data[8] & 0x1f,
      decoderConfig: {
        objectProfileIndication: data[11],
        streamType: (data[12] >>> 2) & 0x3f,
        bufferSize: (data[13] << 16) | (data[14] << 8) | data[15],
        maxBitrate: (data[16] << 24) |
          (data[17] << 16) |
          (data[18] <<  8) |
          data[19],
        avgBitrate: (data[20] << 24) |
          (data[21] << 16) |
          (data[22] <<  8) |
          data[23],
        decoderConfigDescriptor: {
          tag: data[24],
          length: data[25],
          audioObjectType: (data[26] >>> 3) & 0x1f,
          samplingFrequencyIndex: ((data[26] & 0x07) << 1) |
            ((data[27] >>> 7) & 0x01),
          channelConfiguration: (data[27] >>> 3) & 0x0f
        }
      }
    };
  },
  ftyp: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        majorBrand: parseType(data.subarray(0, 4)),
        minorVersion: view.getUint32(4),
        compatibleBrands: []
      },
      i = 8;
    while (i < data.byteLength) {
      result.compatibleBrands.push(parseType(data.subarray(i, i + 4)));
      i += 4;
    }
    return result;
  },
  dinf: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  dref: function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      dataReferences: inspectMp4(data.subarray(8))
    };
  },
  hdlr: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        version: view.getUint8(0),
        flags: new Uint8Array(data.subarray(1, 4)),
        handlerType: parseType(data.subarray(8, 12)),
        name: ''
      },
      i = 8;

    // parse out the name field
    for (i = 24; i < data.byteLength; i++) {
      if (data[i] === 0x00) {
        // the name field is null-terminated
        i++;
        break;
      }
      result.name += String.fromCharCode(data[i]);
    }
    // decode UTF-8 to javascript's internal representation
    // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
    result.name = decodeURIComponent(global.escape(result.name));

    return result;
  },
  mdat: function (data) {
    return {
      byteLength: data.byteLength,
      nals: nalParseAVCC(data),
      size: data.byteLength
    };
  },
  mdhd: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      i = 4,
      language,
      result = {
        version: view.getUint8(0),
        flags: new Uint8Array(data.subarray(1, 4)),
        language: ''
      };
    if (result.version === 1) {
      i += 4;
      result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 8;
      result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 4;
      result.timescale = view.getUint32(i);
      i += 8;
      result.duration = view.getUint32(i); // truncating top 4 bytes
    } else {
      result.creationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.modificationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.timescale = view.getUint32(i);
      i += 4;
      result.duration = view.getUint32(i);
    }
    i += 4;
    // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
    // each field is the packed difference between its ASCII value and 0x60
    language = view.getUint16(i);
    result.language += String.fromCharCode((language >> 10) + 0x60);
    result.language += String.fromCharCode(((language & 0x03c0) >> 5) + 0x60);
    result.language += String.fromCharCode((language & 0x1f) + 0x60);

    return result;
  },
  mdia: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  mfhd: function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      sequenceNumber: (data[4] << 24) |
        (data[5] << 16) |
        (data[6] << 8) |
        (data[7])
    };
  },
  minf: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  // codingname, not a first-class box type. stsd entries share the
  // same format as real boxes so the parsing infrastructure can be
  // shared
  mp4a: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        // 6 bytes reserved
        dataReferenceIndex: view.getUint16(6),
        // 4 + 4 bytes reserved
        channelcount: view.getUint16(16),
        samplesize: view.getUint16(18),
        // 2 bytes pre_defined
        // 2 bytes reserved
        samplerate: view.getUint16(24) + (view.getUint16(26) / 65536)
      };

    // if there are more bytes to process, assume this is an ISO/IEC
    // 14496-14 MP4AudioSampleEntry and parse the ESDBox
    if (data.byteLength > 28) {
      result.streamDescriptor = inspectMp4(data.subarray(28))[0];
    }
    return result;
  },
  moof: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  moov: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  mvex: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  mvhd: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      i = 4,
      result = {
        version: view.getUint8(0),
        flags: new Uint8Array(data.subarray(1, 4))
      };

    if (result.version === 1) {
      i += 4;
      result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 8;
      result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 4;
      result.timescale = view.getUint32(i);
      i += 8;
      result.duration = view.getUint32(i); // truncating top 4 bytes
    } else {
      result.creationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.modificationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.timescale = view.getUint32(i);
      i += 4;
      result.duration = view.getUint32(i);
    }
    i += 4;

    // convert fixed-point, base 16 back to a number
    result.rate = view.getUint16(i) + (view.getUint16(i + 2) / 16);
    i += 4;
    result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8);
    i += 2;
    i += 2;
    i += 2 * 4;
    result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4)));
    i += 9 * 4;
    i += 6 * 4;
    result.nextTrackId = view.getUint32(i);
    return result;
  },
  pdin: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    return {
      version: view.getUint8(0),
      flags: new Uint8Array(data.subarray(1, 4)),
      rate: view.getUint32(4),
      initialDelay: view.getUint32(8)
    };
  },
  sdtp: function (data) {
    var
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        samples: []
      }, i;

    for (i = 4; i < data.byteLength; i++) {
      result.samples.push({
        dependsOn: (data[i] & 0x30) >> 4,
        isDependedOn: (data[i] & 0x0c) >> 2,
        hasRedundancy: data[i] & 0x03
      });
    }
    return result;
  },
  sidx: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
        result = {
          version: data[0],
          flags: new Uint8Array(data.subarray(1, 4)),
          references: [],
          referenceId: view.getUint32(4),
          timescale: view.getUint32(8),
          earliestPresentationTime: view.getUint32(12),
          firstOffset: view.getUint32(16)
        },
        referenceCount = view.getUint16(22),
        i;

    for (i = 24; referenceCount; i += 12, referenceCount--) {
      result.references.push({
        referenceType: (data[i] & 0x80) >>> 7,
        referencedSize: view.getUint32(i) & 0x7FFFFFFF,
        subsegmentDuration: view.getUint32(i + 4),
        startsWithSap: !!(data[i + 8] & 0x80),
        sapType: (data[i + 8] & 0x70) >>> 4,
        sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
      });
    }

    return result;
  },
  smhd: function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      balance: data[4] + (data[5] / 256)
    };
  },
  stbl: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  stco: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        chunkOffsets: []
      },
      entryCount = view.getUint32(4),
      i;
    for (i = 8; entryCount; i += 4, entryCount--) {
      result.chunkOffsets.push(view.getUint32(i));
    }
    return result;
  },
  stsc: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      entryCount = view.getUint32(4),
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        sampleToChunks: []
      },
      i;
    for (i = 8; entryCount; i += 12, entryCount--) {
      result.sampleToChunks.push({
        firstChunk: view.getUint32(i),
        samplesPerChunk: view.getUint32(i + 4),
        sampleDescriptionIndex: view.getUint32(i + 8)
      });
    }
    return result;
  },
  stsd: function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      sampleDescriptions: inspectMp4(data.subarray(8))
    };
  },
  stsz: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        sampleSize: view.getUint32(4),
        entries: []
      },
      i;
    for (i = 12; i < data.byteLength; i += 4) {
      result.entries.push(view.getUint32(i));
    }
    return result;
  },
  stts: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        timeToSamples: []
      },
      entryCount = view.getUint32(4),
      i;

    for (i = 8; entryCount; i += 8, entryCount--) {
      result.timeToSamples.push({
        sampleCount: view.getUint32(i),
        sampleDelta: view.getUint32(i + 4)
      });
    }
    return result;
  },
  styp: function (data) {
    return parse.ftyp(data);
  },
  tfdt: function (data) {
    var result = {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      baseMediaDecodeTime: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
    };
    if (result.version === 1) {
      result.baseMediaDecodeTime *= Math.pow(2, 32);
      result.baseMediaDecodeTime += data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11];
    }
    return result;
  },
  tfhd: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        trackId: view.getUint32(4)
      },
      baseDataOffsetPresent = result.flags[2] & 0x01,
      sampleDescriptionIndexPresent = result.flags[2] & 0x02,
      defaultSampleDurationPresent = result.flags[2] & 0x08,
      defaultSampleSizePresent = result.flags[2] & 0x10,
      defaultSampleFlagsPresent = result.flags[2] & 0x20,
      i;

    i = 8;
    if (baseDataOffsetPresent) {
      i += 4; // truncate top 4 bytes
      result.baseDataOffset = view.getUint32(12);
      i += 4;
    }
    if (sampleDescriptionIndexPresent) {
      result.sampleDescriptionIndex = view.getUint32(i);
      i += 4;
    }
    if (defaultSampleDurationPresent) {
      result.defaultSampleDuration = view.getUint32(i);
      i += 4;
    }
    if (defaultSampleSizePresent) {
      result.defaultSampleSize = view.getUint32(i);
      i += 4;
    }
    if (defaultSampleFlagsPresent) {
      result.defaultSampleFlags = view.getUint32(i);
    }
    return result;
  },
  tkhd: function (data) {
    var
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      i = 4,
      result = {
        version: view.getUint8(0),
        flags: new Uint8Array(data.subarray(1, 4))
      };
    if (result.version === 1) {
      i += 4;
      result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 8;
      result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
      i += 4;
      result.trackId = view.getUint32(i);
      i += 4;
      i += 8;
      result.duration = view.getUint32(i); // truncating top 4 bytes
    } else {
      result.creationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.modificationTime = parseMp4Date(view.getUint32(i));
      i += 4;
      result.trackId = view.getUint32(i);
      i += 4;
      i += 4;
      result.duration = view.getUint32(i);
    }
    i += 4;
    i += 2 * 4;
    result.layer = view.getUint16(i);
    i += 2;
    result.alternateGroup = view.getUint16(i);
    i += 2;
    // convert fixed-point, base 16 back to a number
    result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8);
    i += 2;
    i += 2;
    result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4)));
    i += 9 * 4;
    result.width = view.getUint16(i) + (view.getUint16(i + 2) / 16);
    i += 4;
    result.height = view.getUint16(i) + (view.getUint16(i + 2) / 16);
    return result;
  },
  traf: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  trak: function (data) {
    return {
      boxes: inspectMp4(data)
    };
  },
  trex: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      trackId: view.getUint32(4),
      defaultSampleDescriptionIndex: view.getUint32(8),
      defaultSampleDuration: view.getUint32(12),
      defaultSampleSize: view.getUint32(16),
      sampleDependsOn: data[20] & 0x03,
      sampleIsDependedOn: (data[21] & 0xc0) >> 6,
      sampleHasRedundancy: (data[21] & 0x30) >> 4,
      samplePaddingValue: (data[21] & 0x0e) >> 1,
      sampleIsDifferenceSample: !!(data[21] & 0x01),
      sampleDegradationPriority: view.getUint16(22)
    };
  },
  trun: function (data) {
    var
      result = {
        version: data[0],
        flags: new Uint8Array(data.subarray(1, 4)),
        samples: []
      },
      view = new DataView(data.buffer, data.byteOffset, data.byteLength),
      dataOffsetPresent = result.flags[2] & 0x01,
      firstSampleFlagsPresent = result.flags[2] & 0x04,
      sampleDurationPresent = result.flags[1] & 0x01,
      sampleSizePresent = result.flags[1] & 0x02,
      sampleFlagsPresent = result.flags[1] & 0x04,
      sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
      sampleCount = view.getUint32(4),
      offset = 8,
      sample;

    if (dataOffsetPresent) {
      result.dataOffset = view.getUint32(offset);
      offset += 4;
    }

    if (firstSampleFlagsPresent && sampleCount) {
      sample = {
        flags: parseSampleFlags(data.subarray(offset, offset + 4))
      };
      offset += 4;
      if (sampleDurationPresent) {
        sample.duration = view.getUint32(offset);
        offset += 4;
      }
      if (sampleSizePresent) {
        sample.size = view.getUint32(offset);
        offset += 4;
      }
      if (sampleCompositionTimeOffsetPresent) {
        sample.compositionTimeOffset = view.getUint32(offset);
        offset += 4;
      }
      result.samples.push(sample);
      sampleCount--;
    }

    while (sampleCount--) {
      sample = {};
      if (sampleDurationPresent) {
        sample.duration = view.getUint32(offset);
        offset += 4;
      }
      if (sampleSizePresent) {
        sample.size = view.getUint32(offset);
        offset += 4;
      }
      if (sampleFlagsPresent) {
        sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
        offset += 4;
      }
      if (sampleCompositionTimeOffsetPresent) {
        sample.compositionTimeOffset = view.getUint32(offset);
        offset += 4;
      }
      result.samples.push(sample);
    }
    return result;
  },
  'url ': function (data) {
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4))
    };
  },
  vmhd: function (data) {
    var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    return {
      version: data[0],
      flags: new Uint8Array(data.subarray(1, 4)),
      graphicsmode: view.getUint16(4),
      opcolor: new Uint16Array([view.getUint16(6),
                                view.getUint16(8),
                                view.getUint16(10)])
    };
  }
};


/**
 * Return a javascript array of box objects parsed from an ISO base
 * media file.
 * @param data {Uint8Array} the binary data of the media to be inspected
 * @return {array} a javascript array of potentially nested box objects
 */
const inspectMp4 = function (data) {
  var
    i = 0,
    result = [],
    view,
    size,
    type,
    end,
    box,
    seenMOOV = false,
    pendingMDAT = null;

  // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
  var ab = new ArrayBuffer(data.length);
  var v = new Uint8Array(ab);
  for (var z = 0; z < data.length; ++z) {
      v[z] = data[z];
  }
  view = new DataView(ab);


  while (i < data.byteLength) {
    // parse box data
    size = view.getUint32(i);
    type =  parseType(data.subarray(i + 4, i + 8));
    end = size > 1 ? i + size : data.byteLength;

    if (type === 'moov') {
      seenMOOV = true;
    }

    if (type === 'mdat' && !seenMOOV) {
      pendingMDAT = data.subarray(i + 8, end);
    } else {
      // parse type-specific data
      box = (parse[type] || function (data) {
        return {
          data: data
        };
      })(data.subarray(i + 8, end));
      box.size = size;
      box.type = type;
      // store this box and move to the next
      result.push(box);
    }

    if (pendingMDAT && seenMOOV) {
      box = parse['mdat'](pendingMDAT);
      box.size = pendingMDAT.byteLength;
      box.type = 'mdat';
      // store this box and move to the next
      result.push(box);
      pendingMDAT = null;
    }

    i = end;
  }
  return result;
};

/**
 * Returns a textual representation of the javascript represtentation
 * of an MP4 file. You can use it as an alternative to
 * JSON.stringify() to compare inspected MP4s.
 * @param inspectedMp4 {array} the parsed array of boxes in an MP4
 * file
 * @param depth {number} (optional) the number of ancestor boxes of
 * the elements of inspectedMp4. Assumed to be zero if unspecified.
 * @return {string} a text representation of the parsed MP4
 */
const textifyMp4 = function (inspectedMp4, depth) {
  var indent;
  depth = depth || 0;
  indent = new Array(depth * 2 + 1).join(' ');

  // iterate over all the boxes
  return inspectedMp4.map(function (box, index) {

    // list the box type first at the current indentation level
    return indent + box.type + '\n' +

      // the type is already included and handle child boxes separately
      Object.keys(box).filter(function (key) {
        return key !== 'type' && key !== 'boxes';

      // output all the box properties
      }).map(function (key) {
        var prefix = indent + '  ' + key + ': ',
            value = box[key];

        // print out raw bytes as hexademical
        if (value instanceof Uint8Array || value instanceof Uint32Array) {
          return prefix + dataToHex(value, indent);
        }

        // stringify generic objects
        return prefix +
            JSON.stringify(value, null, 2)
              .split('\n').map(function (line, index) {
                if (index === 0) {
                  return line;
                }
                return indent + '  ' + line;
              }).join('\n');
      }).join('\n') +

    // recursively textify the child boxes
    (box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : '');
  }).join('\n');
};

const domifyMp4 = function (inspectedMp4) {
  var topLevelObject = {
    type: 'mp4',
    boxes: inspectedMp4,
    size: inspectedMp4.reduce((sum, box) => sum + box.size, 0)
  };

  var container = document.createElement('div');

  domifyBox(topLevelObject, container, 1);

  return container;
};

/*
<boxType size="100" flags>
  <properties>
    <name></name><value></value>
    <name></name><value></value>
  </properties>
  <boxes>
  </boxes>
</boxType>
*/

const domifyBox = function (box, parentNode, depth) {
  var isObject = (o) => Object.prototype.toString.call(o) === '[object Object]';
  var attributes = ['size', 'flags', 'type', 'version'];
  var specialProperties = ['boxes', 'nals', 'samples'];
  var objectProperties = Object.keys(box).filter((key) => {
    return isObject(box[key]) ||
      (Array.isArray(box[key]) && isObject(box[key][0]));
  });
  var propertyExclusions =
    attributes
      .concat(specialProperties)
      .concat(objectProperties);
  var subProperties = Object.keys(box).filter((key) => {
    return propertyExclusions.indexOf(key) === -1;
  });

  var boxNode = document.createElement('mp4-box');
  var propertyNode = document.createElement('mp4-properties');
  var subBoxesNode = document.createElement('mp4-boxes');
  var boxTypeNode = document.createElement('mp4-box-type');

  if (box.type) {
    boxTypeNode.textContent = box.type;

    if (depth > 1) {
      boxTypeNode.classList.add('collapsed');
    }

    boxNode.appendChild(boxTypeNode);
  }

  attributes.forEach((key) => {
    if (typeof box[key] !== 'undefined') {
      boxNode.setAttribute('data-' + key, box[key]);
    }
  });

  if (subProperties.length) {
    subProperties.forEach((key) => {
      makeProperty(key, box[key], propertyNode);
    });
    boxNode.appendChild(propertyNode);
  }

  if (box.boxes && box.boxes.length) {
    box.boxes.forEach((subBox) => domifyBox(subBox, subBoxesNode, depth + 1));
    boxNode.appendChild(subBoxesNode);
  } else if (objectProperties.length) {
    objectProperties.forEach((key) => {
      if (Array.isArray(box[key])) {
        domifyBox({
          type: key,
          boxes: box[key],
          size: box[key].size
        },
        subBoxesNode,
        depth + 1);
      } else {
        domifyBox(box[key], subBoxesNode, depth + 1);
      }
    });
    boxNode.appendChild(subBoxesNode);
  }

  parentNode.appendChild(boxNode);
};

const makeProperty = function (name, value, parentNode) {
  var nameNode = document.createElement('mp4-name');
  var valueNode = document.createElement('mp4-value');
  var propertyNode = document.createElement('mp4-property');

  nameNode.setAttribute('data-name', name);
  nameNode.textContent = name;

  if (value instanceof Uint8Array || value instanceof Uint32Array) {
    let strValue = dataToHex(value, '');
    let sliceOffset = 0;
    let lines = 0;

    for (; sliceOffset < strValue.length; sliceOffset++) {
      if (strValue[sliceOffset] === '\n') {
        if (++lines === 21) {
          sliceOffset++;
          break;
        }
      }
    }
    let truncValue = strValue.slice(0, sliceOffset);

    if (truncValue.length < strValue.length) {
      truncValue += '<' + (value.byteLength - 336) + 'b remaining of ' + value.byteLength + 'b total>';
    }

    valueNode.setAttribute('data-value', truncValue.toUpperCase());
    valueNode.innerHTML = truncValue;
    valueNode.classList.add('pre-like');
  } else if (Array.isArray(value)) {
    let strValue = '[' + value.join(', ') + ']';
    valueNode.setAttribute('data-value', strValue);
    valueNode.textContent = strValue;
  } else {
    valueNode.setAttribute('data-value', value);
    valueNode.textContent = value;
  }

  propertyNode.appendChild(nameNode);
  propertyNode.appendChild(valueNode);

  parentNode.appendChild(propertyNode);
};

export default {
  inspect: inspectMp4,
  textify: textifyMp4,
  domify: domifyMp4
};
